/** * Video Utils Unit Tests * * Tests for the video utility module. * Covers URL parsing, embed URL generation, duration formatting, and validation. */ import { describe, it, expect } from 'vitest' import { parseVideoUrl, generateEmbedUrl, formatDuration, parseDuration, getAspectRatioClass, extractVideoId, isValidVideoUrl, getVideoPlatform, getVideoThumbnail, validateVideoUrl, } from '@/lib/video' describe('Video Utils', () => { describe('parseVideoUrl', () => { describe('YouTube URLs', () => { it('parses standard watch URL', () => { const result = parseVideoUrl('https://www.youtube.com/watch?v=dQw4w9WgXcQ') expect(result).not.toBeNull() expect(result?.platform).toBe('youtube') expect(result?.videoId).toBe('dQw4w9WgXcQ') expect(result?.embedUrl).toBe('https://www.youtube.com/embed/dQw4w9WgXcQ') expect(result?.thumbnailUrl).toBe('https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg') }) it('parses short URL (youtu.be)', () => { const result = parseVideoUrl('https://youtu.be/dQw4w9WgXcQ') expect(result?.platform).toBe('youtube') expect(result?.videoId).toBe('dQw4w9WgXcQ') }) it('parses embed URL', () => { const result = parseVideoUrl('https://www.youtube.com/embed/dQw4w9WgXcQ') expect(result?.platform).toBe('youtube') expect(result?.videoId).toBe('dQw4w9WgXcQ') }) it('parses youtube-nocookie URL', () => { const result = parseVideoUrl('https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ') expect(result?.platform).toBe('youtube') expect(result?.videoId).toBe('dQw4w9WgXcQ') }) it('parses shorts URL', () => { const result = parseVideoUrl('https://www.youtube.com/shorts/dQw4w9WgXcQ') expect(result?.platform).toBe('youtube') expect(result?.videoId).toBe('dQw4w9WgXcQ') }) it('parses URL with additional parameters', () => { const result = parseVideoUrl('https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=120&list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf') expect(result?.platform).toBe('youtube') expect(result?.videoId).toBe('dQw4w9WgXcQ') }) it('handles URL without https://', () => { const result = parseVideoUrl('youtube.com/watch?v=dQw4w9WgXcQ') expect(result?.platform).toBe('youtube') expect(result?.videoId).toBe('dQw4w9WgXcQ') }) }) describe('Vimeo URLs', () => { it('parses standard Vimeo URL', () => { const result = parseVideoUrl('https://vimeo.com/123456789') expect(result?.platform).toBe('vimeo') expect(result?.videoId).toBe('123456789') expect(result?.embedUrl).toBe('https://player.vimeo.com/video/123456789') expect(result?.thumbnailUrl).toBeNull() // Vimeo needs API call }) it('parses player URL', () => { const result = parseVideoUrl('https://player.vimeo.com/video/123456789') expect(result?.platform).toBe('vimeo') expect(result?.videoId).toBe('123456789') }) it('parses channel URL', () => { const result = parseVideoUrl('https://vimeo.com/channels/staffpicks/123456789') expect(result?.platform).toBe('vimeo') expect(result?.videoId).toBe('123456789') }) it('parses groups URL', () => { const result = parseVideoUrl('https://vimeo.com/groups/shortfilms/videos/123456789') expect(result?.platform).toBe('vimeo') expect(result?.videoId).toBe('123456789') }) }) describe('External Video URLs', () => { it('recognizes direct MP4 URL', () => { const result = parseVideoUrl('https://example.com/video.mp4') expect(result?.platform).toBe('external') expect(result?.videoId).toBeNull() expect(result?.embedUrl).toBe('https://example.com/video.mp4') }) it('recognizes WebM URL', () => { const result = parseVideoUrl('https://example.com/video.webm') expect(result?.platform).toBe('external') }) it('recognizes MOV URL', () => { const result = parseVideoUrl('https://cdn.example.com/uploads/movie.mov') expect(result?.platform).toBe('external') }) }) describe('Edge Cases', () => { it('returns null for empty string', () => { expect(parseVideoUrl('')).toBeNull() }) it('returns null for null input', () => { expect(parseVideoUrl(null as unknown as string)).toBeNull() }) it('returns null for undefined input', () => { expect(parseVideoUrl(undefined as unknown as string)).toBeNull() }) it('returns unknown for invalid URL', () => { const result = parseVideoUrl('https://example.com/page') expect(result?.platform).toBe('unknown') expect(result?.videoId).toBeNull() expect(result?.embedUrl).toBeNull() }) it('handles whitespace', () => { const result = parseVideoUrl(' https://www.youtube.com/watch?v=dQw4w9WgXcQ ') expect(result?.platform).toBe('youtube') expect(result?.videoId).toBe('dQw4w9WgXcQ') }) }) }) describe('generateEmbedUrl', () => { const youtubeInfo = { platform: 'youtube' as const, videoId: 'dQw4w9WgXcQ', originalUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', embedUrl: 'https://www.youtube.com/embed/dQw4w9WgXcQ', thumbnailUrl: 'https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg', } const vimeoInfo = { platform: 'vimeo' as const, videoId: '123456789', originalUrl: 'https://vimeo.com/123456789', embedUrl: 'https://player.vimeo.com/video/123456789', thumbnailUrl: null, } describe('YouTube', () => { it('generates basic embed URL', () => { const url = generateEmbedUrl(youtubeInfo) expect(url).toContain('youtube.com/embed/dQw4w9WgXcQ') expect(url).toContain('modestbranding=1') }) it('adds autoplay parameter', () => { const url = generateEmbedUrl(youtubeInfo, { autoplay: true }) expect(url).toContain('autoplay=1') }) it('adds mute parameter', () => { const url = generateEmbedUrl(youtubeInfo, { muted: true }) expect(url).toContain('mute=1') }) it('adds loop parameter with playlist', () => { const url = generateEmbedUrl(youtubeInfo, { loop: true }) expect(url).toContain('loop=1') expect(url).toContain('playlist=dQw4w9WgXcQ') }) it('hides controls when specified', () => { const url = generateEmbedUrl(youtubeInfo, { controls: false }) expect(url).toContain('controls=0') }) it('adds start time', () => { const url = generateEmbedUrl(youtubeInfo, { startTime: 120 }) expect(url).toContain('start=120') }) it('uses privacy mode (youtube-nocookie)', () => { const url = generateEmbedUrl(youtubeInfo, { privacyMode: true }) expect(url).toContain('youtube-nocookie.com') expect(url).not.toContain('www.youtube.com') }) it('disables related videos', () => { const url = generateEmbedUrl(youtubeInfo, { showRelated: false }) expect(url).toContain('rel=0') }) it('combines multiple options', () => { const url = generateEmbedUrl(youtubeInfo, { autoplay: true, muted: true, loop: true, privacyMode: true, startTime: 30, }) expect(url).toContain('youtube-nocookie.com') expect(url).toContain('autoplay=1') expect(url).toContain('mute=1') expect(url).toContain('loop=1') expect(url).toContain('start=30') }) }) describe('Vimeo', () => { it('generates basic embed URL', () => { const url = generateEmbedUrl(vimeoInfo) expect(url).toBe('https://player.vimeo.com/video/123456789') }) it('adds autoplay parameter', () => { const url = generateEmbedUrl(vimeoInfo, { autoplay: true }) expect(url).toContain('autoplay=1') }) it('adds muted parameter', () => { const url = generateEmbedUrl(vimeoInfo, { muted: true }) expect(url).toContain('muted=1') }) it('adds loop parameter', () => { const url = generateEmbedUrl(vimeoInfo, { loop: true }) expect(url).toContain('loop=1') }) it('adds start time as hash', () => { const url = generateEmbedUrl(vimeoInfo, { startTime: 60 }) expect(url).toContain('#t=60s') }) }) describe('Edge Cases', () => { it('returns null for null input', () => { expect(generateEmbedUrl(null as never)).toBeNull() }) it('returns null for video info without embed URL', () => { expect(generateEmbedUrl({ ...youtubeInfo, embedUrl: null })).toBeNull() }) it('floors start time to integer', () => { const url = generateEmbedUrl(youtubeInfo, { startTime: 30.5 }) expect(url).toContain('start=30') expect(url).not.toContain('start=30.5') }) }) }) describe('formatDuration', () => { it('formats seconds under a minute', () => { expect(formatDuration(45)).toBe('0:45') }) it('formats minutes and seconds', () => { expect(formatDuration(150)).toBe('2:30') }) it('formats hours, minutes, and seconds', () => { expect(formatDuration(3723)).toBe('1:02:03') }) it('pads single digits', () => { expect(formatDuration(65)).toBe('1:05') expect(formatDuration(3605)).toBe('1:00:05') }) it('handles zero', () => { expect(formatDuration(0)).toBe('0:00') }) it('handles negative numbers', () => { expect(formatDuration(-10)).toBe('0:00') }) it('handles NaN', () => { expect(formatDuration(NaN)).toBe('0:00') }) it('handles non-number input', () => { expect(formatDuration('invalid' as unknown as number)).toBe('0:00') }) }) describe('parseDuration', () => { it('parses MM:SS format', () => { expect(parseDuration('2:30')).toBe(150) }) it('parses HH:MM:SS format', () => { expect(parseDuration('1:02:30')).toBe(3750) }) it('parses seconds only', () => { expect(parseDuration('90')).toBe(90) }) it('parses "Xh Ym Zs" format', () => { expect(parseDuration('1h 30m 45s')).toBe(5445) }) it('parses partial formats', () => { expect(parseDuration('2h')).toBe(7200) expect(parseDuration('30m')).toBe(1800) expect(parseDuration('45s')).toBe(45) expect(parseDuration('1h 30m')).toBe(5400) }) it('handles whitespace', () => { expect(parseDuration(' 2:30 ')).toBe(150) }) it('handles empty string', () => { expect(parseDuration('')).toBe(0) }) it('handles null/undefined', () => { expect(parseDuration(null as unknown as string)).toBe(0) expect(parseDuration(undefined as unknown as string)).toBe(0) }) it('handles invalid input', () => { expect(parseDuration('invalid')).toBe(0) }) }) describe('getAspectRatioClass', () => { it('returns aspect-video for 16:9', () => { expect(getAspectRatioClass('16:9')).toBe('aspect-video') }) it('returns correct class for 4:3', () => { expect(getAspectRatioClass('4:3')).toBe('aspect-[4/3]') }) it('returns aspect-square for 1:1', () => { expect(getAspectRatioClass('1:1')).toBe('aspect-square') }) it('returns correct class for 9:16', () => { expect(getAspectRatioClass('9:16')).toBe('aspect-[9/16]') }) it('returns correct class for 21:9', () => { expect(getAspectRatioClass('21:9')).toBe('aspect-[21/9]') }) it('returns default for unknown ratio', () => { expect(getAspectRatioClass('unknown')).toBe('aspect-video') }) }) describe('extractVideoId', () => { it('extracts YouTube video ID', () => { expect(extractVideoId('https://www.youtube.com/watch?v=dQw4w9WgXcQ')).toBe('dQw4w9WgXcQ') }) it('extracts Vimeo video ID', () => { expect(extractVideoId('https://vimeo.com/123456789')).toBe('123456789') }) it('returns null for external URLs', () => { expect(extractVideoId('https://example.com/video.mp4')).toBeNull() }) it('returns null for invalid URLs', () => { expect(extractVideoId('not-a-url')).toBeNull() }) }) describe('isValidVideoUrl', () => { it('returns true for YouTube URLs', () => { expect(isValidVideoUrl('https://www.youtube.com/watch?v=dQw4w9WgXcQ')).toBe(true) }) it('returns true for Vimeo URLs', () => { expect(isValidVideoUrl('https://vimeo.com/123456789')).toBe(true) }) it('returns true for direct video URLs', () => { expect(isValidVideoUrl('https://example.com/video.mp4')).toBe(true) }) it('returns false for non-video URLs', () => { expect(isValidVideoUrl('https://example.com/page')).toBe(false) }) it('returns false for empty string', () => { expect(isValidVideoUrl('')).toBe(false) }) }) describe('getVideoPlatform', () => { it('returns youtube for YouTube URLs', () => { expect(getVideoPlatform('https://www.youtube.com/watch?v=dQw4w9WgXcQ')).toBe('youtube') }) it('returns vimeo for Vimeo URLs', () => { expect(getVideoPlatform('https://vimeo.com/123456789')).toBe('vimeo') }) it('returns external for direct video URLs', () => { expect(getVideoPlatform('https://example.com/video.mp4')).toBe('external') }) it('returns unknown for non-video URLs', () => { expect(getVideoPlatform('https://example.com/page')).toBe('unknown') }) }) describe('getVideoThumbnail', () => { it('returns YouTube thumbnail in default quality', () => { const url = getVideoThumbnail('https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'default') expect(url).toBe('https://img.youtube.com/vi/dQw4w9WgXcQ/default.jpg') }) it('returns YouTube thumbnail in high quality', () => { const url = getVideoThumbnail('https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'high') expect(url).toBe('https://img.youtube.com/vi/dQw4w9WgXcQ/hqdefault.jpg') }) it('returns YouTube thumbnail in max quality', () => { const url = getVideoThumbnail('https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'max') expect(url).toBe('https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg') }) it('returns null for Vimeo (requires API)', () => { expect(getVideoThumbnail('https://vimeo.com/123456789')).toBeNull() }) it('returns null for external URLs', () => { expect(getVideoThumbnail('https://example.com/video.mp4')).toBeNull() }) it('returns null for invalid URLs', () => { expect(getVideoThumbnail('not-a-url')).toBeNull() }) }) describe('validateVideoUrl', () => { it('returns valid for YouTube URL', () => { const result = validateVideoUrl('https://www.youtube.com/watch?v=dQw4w9WgXcQ') expect(result.valid).toBe(true) expect(result.error).toBeUndefined() }) it('returns valid for Vimeo URL', () => { const result = validateVideoUrl('https://vimeo.com/123456789') expect(result.valid).toBe(true) }) it('returns valid for direct video URL', () => { const result = validateVideoUrl('https://example.com/video.mp4') expect(result.valid).toBe(true) }) it('returns invalid for empty URL', () => { const result = validateVideoUrl('') expect(result.valid).toBe(false) expect(result.error).toBe('URL ist erforderlich') }) it('returns invalid for URL without protocol', () => { const result = validateVideoUrl('youtube.com/watch?v=dQw4w9WgXcQ') expect(result.valid).toBe(false) expect(result.error).toContain('http') }) it('returns invalid for unknown URL format', () => { const result = validateVideoUrl('https://example.com/page') expect(result.valid).toBe(false) expect(result.error).toContain('Unbekanntes Video-Format') }) }) })