playlist-extraction.test.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. /**
  2. * @fileoverview Tests for YouTube Playlist Extraction
  3. * @description Test suite for playlist URL detection and data parsing
  4. */
  5. import { describe, it, expect } from 'vitest'
  6. /**
  7. * PLAYLIST EXTRACTION TESTS
  8. *
  9. * Tests playlist functionality:
  10. * - Playlist URL detection
  11. * - Playlist ID extraction
  12. * - JSON response parsing
  13. * - Error handling
  14. */
  15. describe('Playlist Extraction', () => {
  16. describe('Playlist URL Detection', () => {
  17. it('should detect valid playlist URLs', () => {
  18. const playlistUrls = [
  19. 'https://www.youtube.com/playlist?list=PLtest123',
  20. 'https://youtube.com/playlist?list=PLtest456',
  21. 'https://www.youtube.com/watch?v=abc12345678&list=PLtest789'
  22. ]
  23. playlistUrls.forEach(url => {
  24. expect(url).toMatch(/[?&]list=[\w\-]+/)
  25. })
  26. })
  27. it('should extract playlist ID from URL', () => {
  28. const testCases = [
  29. { url: 'https://www.youtube.com/playlist?list=PLtest12345', expected: 'PLtest12345' },
  30. { url: 'https://youtube.com/playlist?list=PLabc-xyz_123', expected: 'PLabc-xyz_123' },
  31. { url: 'https://www.youtube.com/watch?v=test&list=PLmixed123', expected: 'PLmixed123' }
  32. ]
  33. testCases.forEach(({ url, expected }) => {
  34. const match = url.match(/[?&]list=([\w\-]+)/)
  35. expect(match).not.toBeNull()
  36. expect(match[1]).toBe(expected)
  37. })
  38. })
  39. it('should reject non-playlist URLs', () => {
  40. const nonPlaylistUrls = [
  41. 'https://www.youtube.com/watch?v=abc12345678',
  42. 'https://youtu.be/xyz98765432',
  43. 'https://vimeo.com/123456789',
  44. 'https://youtube.com/shorts/test1234567'
  45. ]
  46. nonPlaylistUrls.forEach(url => {
  47. const match = url.match(/[?&]list=([\w\-]+)/)
  48. expect(match).toBeNull()
  49. })
  50. })
  51. })
  52. describe('Playlist Data Parsing', () => {
  53. it('should parse playlist JSON response with all fields', () => {
  54. const mockJsonLine = JSON.stringify({
  55. id: 'abc12345678',
  56. title: 'Test Video',
  57. url: 'https://www.youtube.com/watch?v=abc12345678',
  58. duration: 300,
  59. thumbnail: 'https://i.ytimg.com/vi/abc12345678/default.jpg',
  60. uploader: 'Test Channel'
  61. })
  62. const parsed = JSON.parse(mockJsonLine)
  63. expect(parsed.id).toBe('abc12345678')
  64. expect(parsed.title).toBe('Test Video')
  65. expect(parsed.url).toBe('https://www.youtube.com/watch?v=abc12345678')
  66. expect(parsed.duration).toBe(300)
  67. expect(parsed.thumbnail).toBe('https://i.ytimg.com/vi/abc12345678/default.jpg')
  68. expect(parsed.uploader).toBe('Test Channel')
  69. })
  70. it('should handle missing optional fields gracefully', () => {
  71. const mockJsonLine = JSON.stringify({
  72. id: 'abc12345678',
  73. title: 'Test Video'
  74. })
  75. const parsed = JSON.parse(mockJsonLine)
  76. expect(parsed.id).toBe('abc12345678')
  77. expect(parsed.title).toBe('Test Video')
  78. expect(parsed.duration).toBeUndefined()
  79. expect(parsed.thumbnail).toBeUndefined()
  80. expect(parsed.uploader).toBeUndefined()
  81. })
  82. it('should parse multiple JSON lines from playlist response', () => {
  83. const mockResponse = `{"id":"video1","title":"First Video"}
  84. {"id":"video2","title":"Second Video"}
  85. {"id":"video3","title":"Third Video"}`
  86. const lines = mockResponse.trim().split('\n')
  87. const videos = lines.map(line => JSON.parse(line))
  88. expect(videos).toHaveLength(3)
  89. expect(videos[0].id).toBe('video1')
  90. expect(videos[1].id).toBe('video2')
  91. expect(videos[2].id).toBe('video3')
  92. })
  93. })
  94. describe('Playlist Response Structure', () => {
  95. it('should create proper video objects from parsed data', () => {
  96. const mockData = {
  97. id: 'abc12345678',
  98. title: 'Test Video',
  99. url: 'https://www.youtube.com/watch?v=abc12345678',
  100. duration: 300,
  101. thumbnail: 'https://i.ytimg.com/vi/abc12345678/default.jpg',
  102. uploader: 'Test Channel'
  103. }
  104. const video = {
  105. id: mockData.id,
  106. title: mockData.title || 'Unknown Title',
  107. url: mockData.url || `https://www.youtube.com/watch?v=${mockData.id}`,
  108. duration: mockData.duration || null,
  109. thumbnail: mockData.thumbnail || null,
  110. uploader: mockData.uploader || mockData.channel || null
  111. }
  112. expect(video.id).toBe('abc12345678')
  113. expect(video.title).toBe('Test Video')
  114. expect(video.url).toBe('https://www.youtube.com/watch?v=abc12345678')
  115. expect(video.duration).toBe(300)
  116. expect(video.thumbnail).toBe('https://i.ytimg.com/vi/abc12345678/default.jpg')
  117. expect(video.uploader).toBe('Test Channel')
  118. })
  119. it('should use fallback values when fields are missing', () => {
  120. const mockData = {
  121. id: 'abc12345678'
  122. }
  123. const video = {
  124. id: mockData.id,
  125. title: mockData.title || 'Unknown Title',
  126. url: mockData.url || `https://www.youtube.com/watch?v=${mockData.id}`,
  127. duration: mockData.duration || null,
  128. thumbnail: mockData.thumbnail || null,
  129. uploader: mockData.uploader || mockData.channel || null
  130. }
  131. expect(video.id).toBe('abc12345678')
  132. expect(video.title).toBe('Unknown Title')
  133. expect(video.url).toBe('https://www.youtube.com/watch?v=abc12345678')
  134. expect(video.duration).toBeNull()
  135. expect(video.thumbnail).toBeNull()
  136. expect(video.uploader).toBeNull()
  137. })
  138. it('should handle channel field as fallback for uploader', () => {
  139. const mockData = {
  140. id: 'abc12345678',
  141. title: 'Test Video',
  142. channel: 'Channel Name'
  143. }
  144. const video = {
  145. id: mockData.id,
  146. title: mockData.title || 'Unknown Title',
  147. url: mockData.url || `https://www.youtube.com/watch?v=${mockData.id}`,
  148. duration: mockData.duration || null,
  149. thumbnail: mockData.thumbnail || null,
  150. uploader: mockData.uploader || mockData.channel || null
  151. }
  152. expect(video.uploader).toBe('Channel Name')
  153. })
  154. })
  155. describe('Playlist Response Validation', () => {
  156. it('should create valid success response structure', () => {
  157. const playlistId = 'PLtest12345'
  158. const videos = [
  159. { id: 'video1', title: 'First' },
  160. { id: 'video2', title: 'Second' }
  161. ]
  162. const response = {
  163. success: true,
  164. playlistId: playlistId,
  165. videoCount: videos.length,
  166. videos: videos
  167. }
  168. expect(response.success).toBe(true)
  169. expect(response.playlistId).toBe('PLtest12345')
  170. expect(response.videoCount).toBe(2)
  171. expect(response.videos).toHaveLength(2)
  172. })
  173. it('should count videos correctly', () => {
  174. const videos = []
  175. for (let i = 0; i < 50; i++) {
  176. videos.push({ id: `video${i}`, title: `Video ${i}` })
  177. }
  178. const response = {
  179. success: true,
  180. playlistId: 'PLtest',
  181. videoCount: videos.length,
  182. videos: videos
  183. }
  184. expect(response.videoCount).toBe(50)
  185. expect(response.videos).toHaveLength(50)
  186. })
  187. })
  188. describe('Error Handling', () => {
  189. it('should validate playlist URL format', () => {
  190. const invalidUrls = [
  191. '',
  192. null,
  193. undefined,
  194. 'https://www.youtube.com/watch?v=abc12345678',
  195. 'https://vimeo.com/123456789'
  196. ]
  197. invalidUrls.forEach(url => {
  198. if (url) {
  199. const match = url.match(/[?&]list=([\w\-]+)/)
  200. expect(match).toBeNull()
  201. }
  202. })
  203. })
  204. it('should handle JSON parse errors gracefully', () => {
  205. const invalidJson = 'not valid json'
  206. expect(() => {
  207. JSON.parse(invalidJson)
  208. }).toThrow()
  209. })
  210. it('should continue parsing when one line fails', () => {
  211. const mockResponse = `{"id":"video1","title":"First Video"}
  212. invalid json line here
  213. {"id":"video3","title":"Third Video"}`
  214. const lines = mockResponse.trim().split('\n')
  215. const videos = []
  216. lines.forEach(line => {
  217. try {
  218. const videoData = JSON.parse(line)
  219. videos.push(videoData)
  220. } catch (error) {
  221. // Skip invalid lines
  222. }
  223. })
  224. expect(videos).toHaveLength(2)
  225. expect(videos[0].id).toBe('video1')
  226. expect(videos[1].id).toBe('video3')
  227. })
  228. })
  229. })