Skip to content

Commit 89930d8

Browse files
committed
fix(frontend): SyncStatusButton error handling
1 parent e8df0cf commit 89930d8

2 files changed

Lines changed: 51 additions & 17 deletions

File tree

frontend/src/shared/components/layout/SyncStatusButton.spec.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'
22
import { mount } from '@vue/test-utils'
33
import SyncStatusButton from './SyncStatusButton.vue'
4-
import { useIsFetching, useQueryClient } from '@tanstack/vue-query'
4+
import { useIsFetching, useQueryClient, onlineManager } from '@tanstack/vue-query'
55
import { useEnhancedToast } from '@/shared/composables/useEnhancedToast'
66

77
// Mock TanStack Query
88
vi.mock('@tanstack/vue-query', () => ({
99
useIsFetching: vi.fn(),
1010
useQueryClient: vi.fn(),
11+
onlineManager: {
12+
isOnline: vi.fn(),
13+
},
1114
}))
1215

1316
// Mock Enhanced Toast
@@ -33,6 +36,7 @@ describe('SyncStatusButton', () => {
3336
}
3437
;(useQueryClient as Mock).mockReturnValue(mockQueryClient)
3538
;(useIsFetching as Mock).mockReturnValue({ value: 0 })
39+
;(onlineManager.isOnline as Mock).mockReturnValue(true)
3640

3741
mockShowSuccess = vi.fn()
3842
mockShowError = vi.fn()
@@ -45,7 +49,7 @@ describe('SyncStatusButton', () => {
4549
vi.spyOn(console, 'error').mockImplementation(() => {})
4650
})
4751

48-
it('should call invalidateQueries and show success toast on click', async () => {
52+
it('should call invalidateQueries and show success toast on click when online', async () => {
4953
vi.useFakeTimers()
5054
const wrapper = mount(SyncStatusButton, {
5155
global: {
@@ -72,15 +76,44 @@ describe('SyncStatusButton', () => {
7276
await vi.advanceTimersByTimeAsync(600)
7377

7478
expect(mockShowSuccess).toHaveBeenCalledWith(
75-
'Data synchronized',
76-
'All active data views have been updated.'
79+
'Data is updated successfully!'
7780
)
7881
vi.useRealTimers()
7982
})
8083

81-
it('should show error toast and mark error as silent on failure', async () => {
84+
it('should show "Sync Paused" error when offline after click', async () => {
8285
vi.useFakeTimers()
83-
const error = new Error('Sync failed')
86+
;(onlineManager.isOnline as Mock).mockReturnValue(false)
87+
88+
const wrapper = mount(SyncStatusButton, {
89+
global: {
90+
stubs: {
91+
TooltipProvider: { template: '<div><slot /></div>' },
92+
Tooltip: { template: '<div><slot /></div>' },
93+
TooltipTrigger: { template: '<div><slot /></div>' },
94+
TooltipContent: { template: '<div><slot /></div>' },
95+
Button: {
96+
template: '<button @click="$emit(\'click\')"><slot /></button>',
97+
props: ['disabled'],
98+
},
99+
},
100+
},
101+
})
102+
103+
const button = wrapper.find('button')
104+
await button.trigger('click')
105+
106+
await vi.advanceTimersByTimeAsync(600)
107+
108+
expect(mockShowError).toHaveBeenCalled()
109+
const errorPassed = mockShowError.mock.calls[0][0]
110+
expect(errorPassed.message).toContain('currently offline')
111+
vi.useRealTimers()
112+
})
113+
114+
it('should NOT call showError on query failure (relying on global handler)', async () => {
115+
vi.useFakeTimers()
116+
const error = new Error('Network error')
84117
mockQueryClient.invalidateQueries.mockRejectedValue(error)
85118

86119
const wrapper = mount(SyncStatusButton, {
@@ -103,8 +136,7 @@ describe('SyncStatusButton', () => {
103136

104137
await vi.advanceTimersByTimeAsync(600)
105138

106-
expect(mockShowError).toHaveBeenCalledWith(error, 'Sync Failed')
107-
expect((error as Error & { silent?: boolean }).silent).toBe(true)
139+
expect(mockShowError).not.toHaveBeenCalled()
108140
vi.useRealTimers()
109141
})
110142
})

frontend/src/shared/components/layout/SyncStatusButton.vue

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import { ref, computed } from 'vue'
3-
import { useIsFetching, useQueryClient } from '@tanstack/vue-query'
3+
import { useIsFetching, useQueryClient, onlineManager } from '@tanstack/vue-query'
44
import { Icon } from '@iconify/vue'
55
import { Button } from '@/shared/ui/button'
66
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/shared/ui/tooltip'
@@ -21,20 +21,22 @@ const handleRefresh = async () => {
2121
isSyncing.value = true
2222
2323
try {
24-
// We wrap invalidateQueries in a Promise.all with a minimum timeout
25-
// to ensure the UI animation feels deliberate and smooth (no "blinks")
2624
await Promise.all([
2725
queryClient.invalidateQueries(),
2826
new Promise(resolve => setTimeout(resolve, 600)),
2927
])
3028
31-
showSuccess('Data synchronized', 'All active data views have been updated.')
32-
} catch (error) {
33-
// Mark as silent to prevent App.vue's global handler from showing a duplicate toast
34-
if (error && typeof error === 'object') {
35-
;(error as { silent?: boolean }).silent = true
29+
if (!onlineManager.isOnline()) {
30+
showError(
31+
new Error('You are currently offline. Please check your internet connection and try again.')
32+
)
33+
return
3634
}
37-
showError(error, 'Sync Failed')
35+
36+
showSuccess('Data is updated successfully!')
37+
} catch {
38+
// We don't call showError here because the globalErrorHandler in App.vue
39+
// is already attached to the queryCache and will handle any query-related errors.
3840
} finally {
3941
isSyncing.value = false
4042
}

0 commit comments

Comments
 (0)