From cee76030be9ff629605901007481adc80a06242a Mon Sep 17 00:00:00 2001 From: "chihao.ou" Date: Wed, 30 Sep 2026 02:45:09 +0800 Subject: [PATCH] fix(announcements): report failed read confirmations accurately --- .../components/common/AnnouncementBell.vue | 7 +++- .../common/__tests__/AnnouncementBell.spec.ts | 42 +++++++++++++++++++ frontend/src/stores/announcements.ts | 2 + 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/common/__tests__/AnnouncementBell.spec.ts diff --git a/frontend/src/components/common/AnnouncementBell.vue b/frontend/src/components/common/AnnouncementBell.vue index e80e449f28..afe06ae15b 100644 --- a/frontend/src/components/common/AnnouncementBell.vue +++ b/frontend/src/components/common/AnnouncementBell.vue @@ -373,14 +373,17 @@ function closeDetail() { async function markAsRead(id: number) { try { - await announcementStore.markAsRead(id) + const marked = await announcementStore.markAsRead(id) + if (!marked) appStore.showError(t('common.unknownError')) + return marked } catch (err: any) { appStore.showError(err?.message || t('common.unknownError')) + return false } } async function markAsReadAndClose(id: number) { - await markAsRead(id) + if (!await markAsRead(id)) return appStore.showSuccess(t('announcements.markedAsRead')) closeDetail() } diff --git a/frontend/src/components/common/__tests__/AnnouncementBell.spec.ts b/frontend/src/components/common/__tests__/AnnouncementBell.spec.ts new file mode 100644 index 0000000000..ce729ff604 --- /dev/null +++ b/frontend/src/components/common/__tests__/AnnouncementBell.spec.ts @@ -0,0 +1,42 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' +import AnnouncementBell from '../AnnouncementBell.vue' +import { useAnnouncementStore } from '@/stores/announcements' + +const { markRead, showError, showSuccess } = vi.hoisted(() => ({ markRead: vi.fn(), showError: vi.fn(), showSuccess: vi.fn() })) +vi.mock('@/api', () => ({ announcementsAPI: { markRead } })) +vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showError, showSuccess }) })) +vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) })) +vi.mock('@/utils/format', () => ({ formatRelativeTime: () => 'now', formatRelativeWithDateTime: () => 'now' })) +enableAutoUnmount(afterEach) +beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + vi.spyOn(console, 'error').mockImplementation(() => {}) + useAnnouncementStore().announcements = [{ id: 1, title: 'Notice', content: 'Details', notify_mode: 'silent', created_at: '2026-09-30', updated_at: '2026-09-30' }] +}) +afterEach(() => { vi.restoreAllMocks(); document.body.style.overflow = '' }) + +describe('announcement read confirmation', () => { + it('keeps a failed read confirmation open and only reports success after retry', async () => { + markRead.mockRejectedValue(new Error('offline')) + const wrapper = mount(AnnouncementBell, { global: { stubs: { Teleport: true, Transition: true, Icon: true } } }) + await wrapper.get('button').trigger('click') + await wrapper.get('.group.relative').trigger('click') + await flushPromises() + const confirm = () => wrapper.findAll('button').find(button => button.text() === 'announcements.markRead')! + await confirm().trigger('click') + await flushPromises() + expect(markRead).toHaveBeenCalledTimes(2) + expect(showSuccess).not.toHaveBeenCalled() + expect(showError).toHaveBeenCalled() + expect(wrapper.find('.markdown-body').exists()).toBe(true) + markRead.mockResolvedValue(undefined) + await confirm().trigger('click') + await flushPromises() + expect(showSuccess).toHaveBeenCalledWith('announcements.markedAsRead') + expect(wrapper.find('.markdown-body').exists()).toBe(false) + expect(useAnnouncementStore().unreadCount).toBe(0) + }) +}) diff --git a/frontend/src/stores/announcements.ts b/frontend/src/stores/announcements.ts index e8dac58ad5..9ebba0b6bd 100644 --- a/frontend/src/stores/announcements.ts +++ b/frontend/src/stores/announcements.ts @@ -96,8 +96,10 @@ export const useAnnouncementStore = defineStore('announcements', () => { if (ann) { ann.read_at = new Date().toISOString() } + return true } catch (err: any) { console.error('Failed to mark announcement as read:', err) + return false } }