import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { ErrorRecoveryStrategy } from '@/composables/useErrorHandling' import { useErrorHandling } from '@/composables/useErrorHandling' describe('useErrorHandling', () => { let errorHandler: ReturnType beforeEach(() => { vi.clearAllMocks() setActivePinia(createPinia()) errorHandler = useErrorHandling() }) describe('wrapWithErrorHandlingAsync', () => { it('should execute action successfully', async () => { const action = vi.fn(async () => 'success') const wrapped = errorHandler.wrapWithErrorHandlingAsync(action) const result = await wrapped() expect(result).toBe('success') expect(action).toHaveBeenCalledOnce() }) it('should call error handler when action throws', async () => { const testError = new Error('test error') const action = vi.fn(async () => { throw testError }) const customErrorHandler = vi.fn() const wrapped = errorHandler.wrapWithErrorHandlingAsync( action, customErrorHandler ) await wrapped() expect(customErrorHandler).toHaveBeenCalledWith(testError) }) it('should call finally handler after success', async () => { const action = vi.fn(async () => 'success') const finallyHandler = vi.fn() const wrapped = errorHandler.wrapWithErrorHandlingAsync( action, undefined, finallyHandler ) await wrapped() expect(finallyHandler).toHaveBeenCalledOnce() }) it('should call finally handler after error', async () => { const action = vi.fn(async () => { throw new Error('test error') }) const finallyHandler = vi.fn() const wrapped = errorHandler.wrapWithErrorHandlingAsync( action, vi.fn(), finallyHandler ) await wrapped() expect(finallyHandler).toHaveBeenCalledOnce() }) describe('error recovery', () => { it('should not use recovery strategy when no error occurs', async () => { const action = vi.fn(async () => 'success') const recoveryStrategy: ErrorRecoveryStrategy = { shouldHandle: vi.fn(() => true), recover: vi.fn() } const wrapped = errorHandler.wrapWithErrorHandlingAsync( action, undefined, undefined, [recoveryStrategy] ) await wrapped() expect(recoveryStrategy.shouldHandle).not.toHaveBeenCalled() expect(recoveryStrategy.recover).not.toHaveBeenCalled() }) it('should use recovery strategy when it matches error', async () => { const testError = new Error('test error') const action = vi.fn(async () => { throw testError }) const recoveryStrategy: ErrorRecoveryStrategy = { shouldHandle: vi.fn((error) => error === testError), recover: vi.fn(async () => { // Recovery succeeds, does nothing }) } const wrapped = errorHandler.wrapWithErrorHandlingAsync( action, vi.fn(), undefined, [recoveryStrategy] ) await wrapped() expect(recoveryStrategy.shouldHandle).toHaveBeenCalledWith(testError) expect(recoveryStrategy.recover).toHaveBeenCalled() }) it('should pass action and args to recovery strategy', async () => { const testError = new Error('test error') const action = vi.fn(async (_arg1: string, _arg2: number) => { throw testError }) const recoveryStrategy: ErrorRecoveryStrategy<[string, number], void> = { shouldHandle: vi.fn(() => true), recover: vi.fn() } const wrapped = errorHandler.wrapWithErrorHandlingAsync( action, vi.fn(), undefined, [recoveryStrategy] ) await wrapped('test', 123) expect(recoveryStrategy.recover).toHaveBeenCalledWith( testError, action, ['test', 123] ) }) it('should retry operation when recovery succeeds', async () => { let attemptCount = 0 const action = vi.fn(async (value: string) => { attemptCount++ if (attemptCount === 1) { throw new Error('first attempt failed') } return `success: ${value}` }) const recoveryStrategy: ErrorRecoveryStrategy<[string], string> = { shouldHandle: vi.fn(() => true), recover: vi.fn(async (_error, retry, args) => { await retry(...args) }) } const wrapped = errorHandler.wrapWithErrorHandlingAsync( action, vi.fn(), undefined, [recoveryStrategy] ) await wrapped('test-value') expect(action).toHaveBeenCalledTimes(2) expect(recoveryStrategy.recover).toHaveBeenCalledOnce() }) it('should not call error handler when recovery succeeds', async () => { const action = vi.fn(async () => { throw new Error('test error') }) const customErrorHandler = vi.fn() const recoveryStrategy: ErrorRecoveryStrategy = { shouldHandle: vi.fn(() => true), recover: vi.fn(async () => { // Recovery succeeds }) } const wrapped = errorHandler.wrapWithErrorHandlingAsync( action, customErrorHandler, undefined, [recoveryStrategy] ) await wrapped() expect(customErrorHandler).not.toHaveBeenCalled() }) it('should call error handler when recovery fails', async () => { const originalError = new Error('original error') const recoveryError = new Error('recovery error') const action = vi.fn(async () => { throw originalError }) const customErrorHandler = vi.fn() const recoveryStrategy: ErrorRecoveryStrategy = { shouldHandle: vi.fn(() => true), recover: vi.fn(async () => { throw recoveryError }) } const wrapped = errorHandler.wrapWithErrorHandlingAsync( action, customErrorHandler, undefined, [recoveryStrategy] ) await wrapped() expect(customErrorHandler).toHaveBeenCalledWith(originalError) }) it('should try multiple recovery strategies in order', async () => { const testError = new Error('test error') const action = vi.fn(async () => { throw testError }) const strategy1: ErrorRecoveryStrategy = { shouldHandle: vi.fn(() => false), recover: vi.fn() } const strategy2: ErrorRecoveryStrategy = { shouldHandle: vi.fn(() => true), recover: vi.fn(async () => { // Recovery succeeds }) } const strategy3: ErrorRecoveryStrategy = { shouldHandle: vi.fn(() => true), recover: vi.fn() } const wrapped = errorHandler.wrapWithErrorHandlingAsync( action, vi.fn(), undefined, [strategy1, strategy2, strategy3] ) await wrapped() expect(strategy1.shouldHandle).toHaveBeenCalledWith(testError) expect(strategy1.recover).not.toHaveBeenCalled() expect(strategy2.shouldHandle).toHaveBeenCalledWith(testError) expect(strategy2.recover).toHaveBeenCalled() // Strategy 3 should not be checked because strategy 2 handled it expect(strategy3.shouldHandle).not.toHaveBeenCalled() expect(strategy3.recover).not.toHaveBeenCalled() }) it('should fall back to error handler when no strategy matches', async () => { const testError = new Error('test error') const action = vi.fn(async () => { throw testError }) const customErrorHandler = vi.fn() const strategy: ErrorRecoveryStrategy = { shouldHandle: vi.fn(() => false), recover: vi.fn() } const wrapped = errorHandler.wrapWithErrorHandlingAsync( action, customErrorHandler, undefined, [strategy] ) await wrapped() expect(strategy.shouldHandle).toHaveBeenCalledWith(testError) expect(strategy.recover).not.toHaveBeenCalled() expect(customErrorHandler).toHaveBeenCalledWith(testError) }) it('should work with synchronous actions', async () => { const testError = new Error('test error') const action = vi.fn(() => { throw testError }) const recoveryStrategy: ErrorRecoveryStrategy = { shouldHandle: vi.fn(() => true), recover: vi.fn(async () => { // Recovery succeeds }) } const wrapped = errorHandler.wrapWithErrorHandlingAsync( action, vi.fn(), undefined, [recoveryStrategy] ) await wrapped() expect(recoveryStrategy.recover).toHaveBeenCalled() }) }) describe('backward compatibility', () => { it('should work without recovery strategies parameter', async () => { const action = vi.fn(async () => 'success') const wrapped = errorHandler.wrapWithErrorHandlingAsync(action) const result = await wrapped() expect(result).toBe('success') }) it('should work with empty recovery strategies array', async () => { const testError = new Error('test error') const action = vi.fn(async () => { throw testError }) const customErrorHandler = vi.fn() const wrapped = errorHandler.wrapWithErrorHandlingAsync( action, customErrorHandler, undefined, [] ) await wrapped() expect(customErrorHandler).toHaveBeenCalledWith(testError) }) }) }) })