mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-20 20:39:30 +00:00
## Summary Debounce the reconnecting toast with a 1.5s grace period so brief WebSocket blips don't flash a persistent error banner. ## Problem After #9543 made all error toasts sticky (no auto-dismiss), brief WebSocket disconnections (<1s) would show a persistent "Reconnecting..." error banner that stays until the `reconnected` event fires. On staging, users see this banner even though jobs are actively executing. ## Changes - Extract reconnection toast logic from `GraphView.vue` into `useReconnectingNotification` composable - Add 1.5s delay (via `useTimeoutFn` from VueUse) before showing the reconnecting toast - If `reconnected` fires within the delay window, suppress both the error and success toasts entirely - Clean up unused `useToast`/`useI18n` imports from `GraphView.vue` ## Testing - Sub-1.5s disconnections: no toast shown - Longer disconnections (>1.5s): error toast shown, cleared with success toast on reconnect - Setting `Comfy.Toast.DisableReconnectingToast`: no toasts shown at all - Multiple rapid `reconnecting` events: only one toast shown 6 unit tests covering all scenarios. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10997-fix-debounce-reconnecting-toast-to-prevent-false-positive-banner-33d6d73d3650810289e8f57c046972f1) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Alexander Brown <drjkl@comfy.org>
139 lines
3.5 KiB
TypeScript
139 lines
3.5 KiB
TypeScript
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)
|
|
})
|
|
})
|