import { act, render, renderHook, waitFor } from '@testing-library/react'
import { toast } from 'sonner'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { registerFunnelErrorToast, ToastErrorTracker } from './toast-errors'
import { useTrackFunnelError } from '@/lib/telemetry/use-track-funnel-error'
const { mockTrack } = vi.hoisted(() => ({ mockTrack: vi.fn() }))
vi.mock('@/lib/telemetry/track', () => ({ useTrack: () => mockTrack }))
describe('ToastErrorTracker', () => {
beforeEach(() => {
mockTrack.mockReset()
vi.spyOn(Math, 'random').mockReturnValue(0)
})
afterEach(() => {
vi.restoreAllMocks()
})
it('tracks an unregistered error toast without funnel properties', async () => {
render()
act(() => {
toast.error('request failed')
})
await waitFor(() =>
expect(mockTrack).toHaveBeenCalledWith('dashboard_error_created', { source: 'toast' })
)
expect(mockTrack).toHaveBeenCalledTimes(1)
})
it('fires a single enriched event for a registered funnel toast', async () => {
render()
act(() => {
registerFunnelErrorToast(toast.error('funnel error'), {
origin: 'signup',
errorCategory: 'api',
errorReason: 'other',
errorCode: 500,
})
})
await waitFor(() =>
expect(mockTrack).toHaveBeenCalledWith('dashboard_error_created', {
source: 'toast',
origin: 'signup',
errorCategory: 'api',
errorReason: 'other',
errorCode: 500,
})
)
expect(mockTrack).toHaveBeenCalledTimes(1)
})
it('routes toast-sourced funnel errors from useTrackFunnelError into one enriched event', async () => {
render()
const { result } = renderHook(() => useTrackFunnelError())
act(() => {
const toastId = toast.error('funnel failure')
result.current(
'project_creation',
{ errorCategory: 'api', errorReason: 'rate_limited', errorCode: 429 },
'toast',
toastId
)
})
await waitFor(() =>
expect(mockTrack).toHaveBeenCalledWith('dashboard_error_created', {
source: 'toast',
origin: 'project_creation',
errorCategory: 'api',
errorReason: 'rate_limited',
errorCode: 429,
})
)
expect(mockTrack).toHaveBeenCalledTimes(1)
})
it('tracks a loading toast updated to an error exactly once (sign-in reuses the loading toast id)', async () => {
render()
let toastId: string | number
act(() => {
toastId = toast.loading('Signing in...')
})
act(() => {
toast.error('Invalid login credentials', { id: toastId })
registerFunnelErrorToast(toastId, {
origin: 'signin',
errorCategory: 'api',
errorReason: 'invalid_credentials',
errorCode: 400,
})
})
await waitFor(() =>
expect(mockTrack).toHaveBeenCalledWith('dashboard_error_created', {
source: 'toast',
origin: 'signin',
errorCategory: 'api',
errorReason: 'invalid_credentials',
errorCode: 400,
})
)
expect(mockTrack).toHaveBeenCalledTimes(1)
})
it('ignores non-error toasts', async () => {
render()
act(() => {
toast.success('all good')
})
act(() => {
toast.error('unmarked sentinel')
})
await waitFor(() => expect(mockTrack).toHaveBeenCalledTimes(1))
})
it('skips error toasts that lose the sampling draw', async () => {
vi.spyOn(Math, 'random').mockReturnValueOnce(0.5)
render()
act(() => {
toast.error('sampled out')
})
act(() => {
toast.error('sampled in')
})
await waitFor(() => expect(mockTrack).toHaveBeenCalledTimes(1))
})
})