error-handling.test.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. /**
  2. * @fileoverview Tests for error handling system
  3. * @author GrabZilla Development Team
  4. * @version 2.1.0
  5. */
  6. import { describe, it, expect, beforeEach, vi } from 'vitest'
  7. import { JSDOM } from 'jsdom'
  8. // Mock Electron API
  9. const mockElectronAPI = {
  10. showNotification: vi.fn(),
  11. showErrorDialog: vi.fn(),
  12. showInfoDialog: vi.fn()
  13. }
  14. describe('Error Handling System', () => {
  15. let dom
  16. let window
  17. let document
  18. let ErrorHandler
  19. let errorHandler
  20. let ERROR_TYPES
  21. beforeEach(() => {
  22. // Set up DOM environment
  23. dom = new JSDOM(`
  24. <!DOCTYPE html>
  25. <html>
  26. <body>
  27. <div id="app"></div>
  28. </body>
  29. </html>
  30. `, {
  31. url: 'http://localhost',
  32. pretendToBeVisual: true,
  33. resources: 'usable'
  34. })
  35. window = dom.window
  36. document = window.document
  37. global.window = window
  38. global.document = document
  39. // Mock APIs
  40. window.electronAPI = mockElectronAPI
  41. // Mock logger for error-handler.js
  42. window.logger = {
  43. error: vi.fn(),
  44. warn: vi.fn(),
  45. info: vi.fn(),
  46. debug: vi.fn()
  47. }
  48. // Load the error handler
  49. const fs = require('fs')
  50. const path = require('path')
  51. const errorHandlerScript = fs.readFileSync(
  52. path.join(__dirname, '../scripts/utils/error-handler.js'),
  53. 'utf8'
  54. )
  55. // Execute the script in the window context
  56. const script = new window.Function(errorHandlerScript)
  57. script.call(window)
  58. ErrorHandler = window.ErrorHandler
  59. errorHandler = window.errorHandler
  60. ERROR_TYPES = window.ERROR_TYPES
  61. })
  62. describe('ErrorHandler', () => {
  63. it('should initialize with correct default values', () => {
  64. const handler = new ErrorHandler()
  65. expect(handler.errorHistory).toEqual([])
  66. expect(handler.maxHistorySize).toBe(50)
  67. expect(handler.notificationQueue).toEqual([])
  68. })
  69. it('should parse network errors correctly', async () => {
  70. const error = new Error('Network connection failed')
  71. const errorInfo = await errorHandler.handleError(error)
  72. expect(errorInfo.type).toBe(ERROR_TYPES.NETWORK)
  73. expect(errorInfo.message).toBe('Network connection error')
  74. expect(errorInfo.recoverable).toBe(true)
  75. })
  76. it('should parse binary missing errors correctly', async () => {
  77. const error = new Error('yt-dlp binary not found')
  78. const errorInfo = await errorHandler.handleError(error)
  79. expect(errorInfo.type).toBe(ERROR_TYPES.BINARY_MISSING)
  80. expect(errorInfo.message).toBe('Required application components are missing')
  81. expect(errorInfo.recoverable).toBe(false)
  82. })
  83. it('should parse permission errors correctly', async () => {
  84. const error = new Error('Permission denied - not writable')
  85. const errorInfo = await errorHandler.handleError(error)
  86. expect(errorInfo.type).toBe(ERROR_TYPES.PERMISSION)
  87. expect(errorInfo.message).toBe('Permission denied')
  88. expect(errorInfo.recoverable).toBe(true)
  89. })
  90. it('should parse video unavailable errors correctly', async () => {
  91. const error = new Error('Video is unavailable or private')
  92. const errorInfo = await errorHandler.handleError(error)
  93. expect(errorInfo.type).toBe(ERROR_TYPES.VIDEO_UNAVAILABLE)
  94. expect(errorInfo.message).toBe('Video is unavailable or has been removed')
  95. expect(errorInfo.recoverable).toBe(false)
  96. })
  97. it('should parse age-restricted errors correctly', async () => {
  98. const error = new Error('Sign in to confirm your age')
  99. const errorInfo = await errorHandler.handleError(error)
  100. expect(errorInfo.type).toBe(ERROR_TYPES.AGE_RESTRICTED)
  101. expect(errorInfo.message).toBe('Age-restricted content requires authentication')
  102. expect(errorInfo.recoverable).toBe(true)
  103. })
  104. it('should parse disk space errors correctly', async () => {
  105. const error = new Error('No space left on device')
  106. const errorInfo = await errorHandler.handleError(error)
  107. expect(errorInfo.type).toBe(ERROR_TYPES.DISK_SPACE)
  108. expect(errorInfo.message).toBe('Insufficient disk space')
  109. expect(errorInfo.recoverable).toBe(true)
  110. })
  111. it('should parse format errors correctly', async () => {
  112. const error = new Error('Requested format not available')
  113. const errorInfo = await errorHandler.handleError(error)
  114. expect(errorInfo.type).toBe(ERROR_TYPES.FORMAT_ERROR)
  115. expect(errorInfo.message).toBe('Requested video quality or format not available')
  116. expect(errorInfo.recoverable).toBe(true)
  117. })
  118. it('should add errors to history', async () => {
  119. const error = new Error('Test error')
  120. await errorHandler.handleError(error)
  121. expect(errorHandler.errorHistory).toHaveLength(1)
  122. expect(errorHandler.errorHistory[0].originalError).toBe(error)
  123. })
  124. it('should limit error history size', async () => {
  125. const handler = new ErrorHandler()
  126. handler.maxHistorySize = 3
  127. // Add more errors than the limit
  128. for (let i = 0; i < 5; i++) {
  129. await handler.handleError(new Error(`Error ${i}`))
  130. }
  131. expect(handler.errorHistory).toHaveLength(3)
  132. })
  133. it('should generate unique error IDs', () => {
  134. const id1 = errorHandler.generateErrorId()
  135. const id2 = errorHandler.generateErrorId()
  136. expect(id1).not.toBe(id2)
  137. expect(id1).toMatch(/^error_\d+_[a-z0-9]+$/)
  138. })
  139. it('should show error notifications', async () => {
  140. mockElectronAPI.showNotification.mockResolvedValue({ success: true })
  141. const error = new Error('Test error')
  142. await errorHandler.handleError(error, {}, { showNotification: true })
  143. expect(mockElectronAPI.showNotification).toHaveBeenCalled()
  144. })
  145. it('should show error dialogs for critical errors', async () => {
  146. mockElectronAPI.showErrorDialog.mockResolvedValue({ success: true, response: 0 })
  147. const error = new Error('Critical error')
  148. await errorHandler.handleError(error, {}, { showDialog: true })
  149. expect(mockElectronAPI.showErrorDialog).toHaveBeenCalled()
  150. })
  151. it('should dispatch in-app error events', async () => {
  152. let eventFired = false
  153. let eventDetail = null
  154. document.addEventListener('app-error', (event) => {
  155. eventFired = true
  156. eventDetail = event.detail
  157. })
  158. const error = new Error('Test error')
  159. await errorHandler.handleError(error, {}, { showInUI: true })
  160. expect(eventFired).toBe(true)
  161. expect(eventDetail).toHaveProperty('message')
  162. expect(eventDetail).toHaveProperty('type')
  163. })
  164. it('should handle binary errors specifically', async () => {
  165. mockElectronAPI.showNotification.mockResolvedValue({ success: true })
  166. mockElectronAPI.showErrorDialog.mockResolvedValue({ success: true, response: 0 })
  167. const errorInfo = await errorHandler.handleBinaryError('yt-dlp')
  168. expect(errorInfo.type).toBe(ERROR_TYPES.BINARY_MISSING)
  169. expect(errorInfo.message).toContain('yt-dlp')
  170. expect(mockElectronAPI.showNotification).toHaveBeenCalled()
  171. expect(mockElectronAPI.showErrorDialog).toHaveBeenCalled()
  172. })
  173. it('should handle network errors with retry logic', async () => {
  174. mockElectronAPI.showErrorDialog.mockResolvedValue({ success: true, response: 0 })
  175. const retryCallback = vi.fn().mockResolvedValue('success')
  176. const error = new Error('Network timeout')
  177. const result = await errorHandler.handleNetworkError(error, retryCallback, 1)
  178. expect(retryCallback).toHaveBeenCalled()
  179. })
  180. it('should provide error statistics', () => {
  181. // Add some test errors
  182. errorHandler.errorHistory = [
  183. { type: ERROR_TYPES.NETWORK, recoverable: true, timestamp: new Date() },
  184. { type: ERROR_TYPES.BINARY_MISSING, recoverable: false, timestamp: new Date() },
  185. { type: ERROR_TYPES.NETWORK, recoverable: true, timestamp: new Date() }
  186. ]
  187. const stats = errorHandler.getStats()
  188. expect(stats.total).toBe(3)
  189. expect(stats.byType.network).toBe(2)
  190. expect(stats.byType.binary_missing).toBe(1)
  191. expect(stats.recoverable).toBe(2)
  192. })
  193. it('should clear error history', () => {
  194. errorHandler.errorHistory = [{ test: 'error' }]
  195. errorHandler.clearHistory()
  196. expect(errorHandler.errorHistory).toEqual([])
  197. })
  198. it('should check if errors are recoverable', () => {
  199. const recoverableError = { recoverable: true }
  200. const nonRecoverableError = { recoverable: false }
  201. expect(errorHandler.isRecoverable(recoverableError)).toBe(true)
  202. expect(errorHandler.isRecoverable(nonRecoverableError)).toBe(false)
  203. })
  204. })
  205. describe('Error Types', () => {
  206. it('should have all required error types', () => {
  207. expect(ERROR_TYPES).toHaveProperty('NETWORK')
  208. expect(ERROR_TYPES).toHaveProperty('BINARY_MISSING')
  209. expect(ERROR_TYPES).toHaveProperty('PERMISSION')
  210. expect(ERROR_TYPES).toHaveProperty('VIDEO_UNAVAILABLE')
  211. expect(ERROR_TYPES).toHaveProperty('AGE_RESTRICTED')
  212. expect(ERROR_TYPES).toHaveProperty('DISK_SPACE')
  213. expect(ERROR_TYPES).toHaveProperty('FORMAT_ERROR')
  214. expect(ERROR_TYPES).toHaveProperty('UNKNOWN')
  215. })
  216. it('should have correct configuration for each type', () => {
  217. expect(ERROR_TYPES.NETWORK.recoverable).toBe(true)
  218. expect(ERROR_TYPES.BINARY_MISSING.recoverable).toBe(false)
  219. expect(ERROR_TYPES.PERMISSION.recoverable).toBe(true)
  220. expect(ERROR_TYPES.VIDEO_UNAVAILABLE.recoverable).toBe(false)
  221. })
  222. })
  223. })