diff --git a/src/composables/useReconnectingNotification.test.ts b/src/composables/useReconnectingNotification.test.ts new file mode 100644 index 0000000000..d8485231a2 --- /dev/null +++ b/src/composables/useReconnectingNotification.test.ts @@ -0,0 +1,138 @@ +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { useReconnectingNotification } from '@/composables/useReconnectingNotification' + +const mockToastAdd = vi.fn() +const mockToastRemove = vi.fn() + +vi.mock('primevue/usetoast', () => ({ + useToast: () => ({ + add: mockToastAdd, + remove: mockToastRemove + }) +})) + +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + t: (key: string) => key + }) +})) + +const settingMocks = vi.hoisted(() => ({ + disableToast: false +})) + +vi.mock('@/platform/settings/settingStore', () => ({ + useSettingStore: vi.fn(() => ({ + get: vi.fn((key: string) => { + if (key === 'Comfy.Toast.DisableReconnectingToast') + return settingMocks.disableToast + return undefined + }) + })) +})) + +describe('useReconnectingNotification', () => { + beforeEach(() => { + setActivePinia(createTestingPinia({ stubActions: false })) + vi.useFakeTimers() + vi.clearAllMocks() + settingMocks.disableToast = false + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('does not show toast immediately on reconnecting', () => { + const { onReconnecting } = useReconnectingNotification() + + onReconnecting() + + expect(mockToastAdd).not.toHaveBeenCalled() + }) + + it('shows error toast after delay', () => { + const { onReconnecting } = useReconnectingNotification() + + onReconnecting() + vi.advanceTimersByTime(1500) + + expect(mockToastAdd).toHaveBeenCalledWith( + expect.objectContaining({ + severity: 'error', + summary: 'g.reconnecting' + }) + ) + }) + + it('suppresses toast when reconnected before delay expires', () => { + const { onReconnecting, onReconnected } = useReconnectingNotification() + + onReconnecting() + vi.advanceTimersByTime(500) + onReconnected() + vi.advanceTimersByTime(1500) + + expect(mockToastAdd).not.toHaveBeenCalled() + expect(mockToastRemove).not.toHaveBeenCalled() + }) + + it('removes toast and shows success when reconnected after delay', () => { + const { onReconnecting, onReconnected } = useReconnectingNotification() + + onReconnecting() + vi.advanceTimersByTime(1500) + mockToastAdd.mockClear() + + onReconnected() + + expect(mockToastRemove).toHaveBeenCalledWith( + expect.objectContaining({ + severity: 'error', + summary: 'g.reconnecting' + }) + ) + expect(mockToastAdd).toHaveBeenCalledWith( + expect.objectContaining({ + severity: 'success', + summary: 'g.reconnected', + life: 2000 + }) + ) + }) + + it('does nothing when toast is disabled via setting', () => { + settingMocks.disableToast = true + const { onReconnecting, onReconnected } = useReconnectingNotification() + + onReconnecting() + vi.advanceTimersByTime(1500) + onReconnected() + + expect(mockToastAdd).not.toHaveBeenCalled() + expect(mockToastRemove).not.toHaveBeenCalled() + }) + + it('does nothing when onReconnected is called without prior onReconnecting', () => { + const { onReconnected } = useReconnectingNotification() + + onReconnected() + + expect(mockToastAdd).not.toHaveBeenCalled() + expect(mockToastRemove).not.toHaveBeenCalled() + }) + + it('handles multiple reconnecting events without duplicating toasts', () => { + const { onReconnecting } = useReconnectingNotification() + + onReconnecting() + vi.advanceTimersByTime(1500) // first toast fires + onReconnecting() // second reconnecting event + vi.advanceTimersByTime(1500) // second toast fires + + expect(mockToastAdd).toHaveBeenCalledTimes(2) + }) +}) diff --git a/src/composables/useReconnectingNotification.ts b/src/composables/useReconnectingNotification.ts new file mode 100644 index 0000000000..605bf3bf1c --- /dev/null +++ b/src/composables/useReconnectingNotification.ts @@ -0,0 +1,52 @@ +import { useTimeoutFn } from '@vueuse/core' +import type { ToastMessageOptions } from 'primevue/toast' +import { useToast } from 'primevue/usetoast' +import { ref } from 'vue' +import { useI18n } from 'vue-i18n' + +import { useSettingStore } from '@/platform/settings/settingStore' + +const RECONNECT_TOAST_DELAY_MS = 1500 + +export function useReconnectingNotification() { + const { t } = useI18n() + const toast = useToast() + const settingStore = useSettingStore() + + const reconnectingMessage: ToastMessageOptions = { + severity: 'error', + summary: t('g.reconnecting') + } + + const reconnectingToastShown = ref(false) + + const { start, stop } = useTimeoutFn( + () => { + toast.add(reconnectingMessage) + reconnectingToastShown.value = true + }, + RECONNECT_TOAST_DELAY_MS, + { immediate: false } + ) + + function onReconnecting() { + if (settingStore.get('Comfy.Toast.DisableReconnectingToast')) return + start() + } + + function onReconnected() { + stop() + + if (reconnectingToastShown.value) { + toast.remove(reconnectingMessage) + toast.add({ + severity: 'success', + summary: t('g.reconnected'), + life: 2000 + }) + reconnectingToastShown.value = false + } + } + + return { onReconnecting, onReconnected } +} diff --git a/src/views/GraphView.vue b/src/views/GraphView.vue index 90eec04b7d..0a11b902ae 100644 --- a/src/views/GraphView.vue +++ b/src/views/GraphView.vue @@ -33,8 +33,7 @@