fix(hub,web): harden notification preference updates

This commit is contained in:
2026-08-05 11:16:17 +08:00
parent dac45edc41
commit 0d3eabeaee
4 changed files with 52 additions and 6 deletions
@@ -63,6 +63,16 @@ describe('PUT /api/notification-preferences', () => {
expect(res.status).toBe(400)
})
it('rejects fractional preference values', async () => {
const app = createApp()
const res = await app.request('/api/notification-preferences', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ permissionRequests: 0.5 })
})
expect(res.status).toBe(400)
})
it('rejects invalid bodies', async () => {
const app = createApp()
const res = await app.request('/api/notification-preferences', {
@@ -4,10 +4,10 @@ import type { Store } from '../../store'
import type { WebAppEnv } from '../middleware/auth'
const updateSchema = z.object({
permissionRequests: z.number().min(0).max(1).optional(),
sessionReady: z.number().min(0).max(1).optional(),
taskNotifications: z.number().min(0).max(1).optional(),
sessionCompletion: z.number().min(0).max(1).optional()
permissionRequests: z.number().int().min(0).max(1).optional(),
sessionReady: z.number().int().min(0).max(1).optional(),
taskNotifications: z.number().int().min(0).max(1).optional(),
sessionCompletion: z.number().int().min(0).max(1).optional()
})
export function createNotificationPreferencesRoutes(store: Store): Hono<WebAppEnv> {
+36 -1
View File
@@ -1,7 +1,8 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { I18nProvider } from '@/lib/i18n-context'
import { queryKeys } from '@/lib/query-keys'
import SettingsNotificationsPage from './notifications'
const defaultPrefs = {
@@ -66,6 +67,7 @@ describe('SettingsNotificationsPage', () => {
</I18nProvider>
</QueryClientProvider>,
)
return queryClient
}
it('renders all four toggles from server preferences', async () => {
@@ -105,6 +107,16 @@ describe('SettingsNotificationsPage', () => {
})
})
it('keeps the confirmation open when disabling permission notifications fails', async () => {
updateNotificationPreferences.mockRejectedValueOnce(new Error('save failed'))
renderPage()
fireEvent.click(await screen.findByLabelText('Permission requests'))
fireEvent.click(screen.getByText('Turn off anyway'))
expect(await screen.findByText('save failed')).toBeTruthy()
expect(screen.getByText('Turn off permission request notifications?')).toBeTruthy()
})
it('sends a test push and reports success', async () => {
renderPage()
const button = await screen.findByRole('button', { name: 'Send test push' })
@@ -165,6 +177,29 @@ describe('SettingsNotificationsPage', () => {
expect(await screen.findByText('Failed to save notification copy')).toBeTruthy()
})
it('accepts refreshed copy after a successful save', async () => {
updateNotificationCopy.mockResolvedValueOnce({
...defaultCopyResponse,
copy: { ready: { title: 'Saved title', body: 'Saved body' } }
})
const queryClient = renderPage()
const readyRow = await screen.findByRole('button', { name: /Session ready.*Ready for input/ })
fireEvent.click(readyRow)
fireEvent.change(screen.getByLabelText('Title'), { target: { value: 'Saved title' } })
fireEvent.change(screen.getByLabelText('Body'), { target: { value: 'Saved body' } })
fireEvent.click(screen.getByRole('button', { name: 'Save copy' }))
expect(await screen.findByText('Copy saved')).toBeTruthy()
act(() => {
queryClient.setQueryData(queryKeys.notificationCopy, {
...defaultCopyResponse,
copy: { ready: { title: 'Remote title', body: 'Remote body' } }
})
})
expect(await screen.findByRole('button', { name: /Session ready.*Remote title.*Remote body/ })).toBeTruthy()
})
it('keeps copy editors collapsed and prefills defaults when opened', async () => {
renderPage()
const readyRow = await screen.findByRole('button', { name: /Session ready.*Ready for input/ })
+2 -1
View File
@@ -147,6 +147,7 @@ export default function SettingsNotificationsPage() {
setCopySaveError(null)
},
onSuccess: (data) => {
userEditedRef.current = false
queryClient.setQueryData(queryKeys.notificationCopy, data)
setDraft(resolveEffectiveCopy(data.copy, data.defaults))
setCopySaveError(null)
@@ -336,7 +337,7 @@ export default function SettingsNotificationsPage() {
confirmLabel={t('settings.notifications.disablePermissionConfirm')}
confirmingLabel={t('settings.notifications.disablePermissionConfirm')}
onConfirm={async () => {
mutation.mutate({ permissionRequests: 0 })
await mutation.mutateAsync({ permissionRequests: 0 })
}}
isPending={mutation.isPending}
destructive