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)) }) })