cms.c2sgmbh/tests/unit/youtube/analytics-helpers.unit.spec.ts
Martin Porwoll 9e7b433cd0 feat(youtube): add comparison, trends, ROI analytics
Add analytics helper functions (calculateComparison, calculateTrends,
calculateROI) and extend the analytics API route with three new tabs
for video metric comparison, trend analysis, and ROI calculation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 13:37:51 +00:00

239 lines
7.3 KiB
TypeScript

/**
* YouTube Analytics Helpers Unit Tests
*
* Tests comparison, trend, and ROI calculation functions
* used by the analytics API route.
*/
import { describe, it, expect } from 'vitest'
import {
calculateComparison,
calculateTrends,
calculateROI,
} from '@/lib/youtube/analytics-helpers'
import type { VideoWithPerformance } from '@/lib/youtube/analytics-helpers'
const mockVideos: VideoWithPerformance[] = [
{
id: 1,
title: 'Video A',
performance: { views: 1000, likes: 50, ctr: 5.2, watchTimeMinutes: 300 },
costs: { estimatedProductionCost: 100, estimatedRevenue: 200 },
},
{
id: 2,
title: 'Video B',
performance: { views: 2000, likes: 80, ctr: 3.1, watchTimeMinutes: 600 },
costs: { estimatedProductionCost: 150, estimatedRevenue: 100 },
},
{
id: 3,
title: 'Video C',
performance: { views: 500, likes: 20, ctr: 7.0, watchTimeMinutes: 150 },
costs: { estimatedProductionCost: 50, estimatedRevenue: 75 },
},
]
describe('Analytics Helpers', () => {
describe('calculateComparison', () => {
it('should return comparison metrics for all videos', () => {
const result = calculateComparison(mockVideos, 'views')
expect(result).toHaveLength(3)
expect(result[0]).toEqual({ videoId: 1, title: 'Video A', value: 1000 })
expect(result[1]).toEqual({ videoId: 2, title: 'Video B', value: 2000 })
expect(result[2]).toEqual({ videoId: 3, title: 'Video C', value: 500 })
})
it('should handle different metrics', () => {
const result = calculateComparison(mockVideos, 'likes')
expect(result[0].value).toBe(50)
expect(result[1].value).toBe(80)
expect(result[2].value).toBe(20)
})
it('should return 0 for missing metric values', () => {
const sparse: VideoWithPerformance[] = [
{ id: 10, title: 'Sparse', performance: {} },
]
const result = calculateComparison(sparse, 'impressions')
expect(result).toHaveLength(1)
expect(result[0].value).toBe(0)
})
it('should return an empty array for empty input', () => {
const result = calculateComparison([], 'views')
expect(result).toEqual([])
})
})
describe('calculateTrends', () => {
it('should detect an upward trend when latest exceeds average', () => {
const result = calculateTrends(mockVideos, 'views')
expect(result.trend).toBeDefined()
expect(result.average).toBeDefined()
expect(result.latest).toBeDefined()
expect(result.growth).toBeDefined()
expect(result.min).toBeDefined()
expect(result.max).toBeDefined()
})
it('should return insufficient_data for fewer than 2 videos', () => {
const result = calculateTrends([mockVideos[0]], 'views')
expect(result.trend).toBe('insufficient_data')
expect(result.growth).toBe(0)
expect(result.average).toBeUndefined()
})
it('should return insufficient_data for an empty array', () => {
const result = calculateTrends([], 'views')
expect(result.trend).toBe('insufficient_data')
expect(result.growth).toBe(0)
})
it('should compute correct average and min/max', () => {
const result = calculateTrends(mockVideos, 'views')
// Values sorted: 500, 1000, 2000
// avg = (500 + 1000 + 2000) / 3 = ~1166.67
const expectedAvg = (500 + 1000 + 2000) / 3
expect(result.average).toBeCloseTo(expectedAvg)
expect(result.min).toBe(500)
expect(result.max).toBe(2000)
})
it('should calculate growth percentage relative to average', () => {
const result = calculateTrends(mockVideos, 'views')
const expectedAvg = (500 + 1000 + 2000) / 3
const expectedGrowth = ((2000 - expectedAvg) / expectedAvg) * 100
expect(result.growth).toBeCloseTo(expectedGrowth)
})
it('should return stable trend when latest equals average', () => {
const equal: VideoWithPerformance[] = [
{ id: 1, title: 'A', performance: { views: 100 } },
{ id: 2, title: 'B', performance: { views: 100 } },
]
const result = calculateTrends(equal, 'views')
expect(result.trend).toBe('stable')
expect(result.growth).toBe(0)
})
it('should handle zero values gracefully', () => {
const zeros: VideoWithPerformance[] = [
{ id: 1, title: 'A', performance: { views: 0 } },
{ id: 2, title: 'B', performance: { views: 0 } },
]
const result = calculateTrends(zeros, 'views')
expect(result.trend).toBe('stable')
expect(result.growth).toBe(0)
expect(result.average).toBe(0)
})
})
describe('calculateROI', () => {
it('should calculate ROI for videos with cost data', () => {
const result = calculateROI(mockVideos)
expect(result).toHaveLength(3)
})
it('should compute correct ROI percentage', () => {
const result = calculateROI(mockVideos)
const videoA = result.find((r) => r.videoId === 1)!
// ROI = (200 - 100) / 100 * 100 = 100%
expect(videoA.roi).toBe(100)
expect(videoA.cost).toBe(100)
expect(videoA.revenue).toBe(200)
})
it('should compute correct cost-per-view', () => {
const result = calculateROI(mockVideos)
const videoA = result.find((r) => r.videoId === 1)!
// CPV = 100 / 1000 = 0.1
expect(videoA.cpv).toBeCloseTo(0.1)
})
it('should compute correct revenue-per-view', () => {
const result = calculateROI(mockVideos)
const videoB = result.find((r) => r.videoId === 2)!
// revenuePerView = 100 / 2000 = 0.05
expect(videoB.revenuePerView).toBeCloseTo(0.05)
})
it('should handle negative ROI', () => {
const result = calculateROI(mockVideos)
const videoB = result.find((r) => r.videoId === 2)!
// ROI = (100 - 150) / 150 * 100 = -33.33%
expect(videoB.roi).toBeCloseTo(-33.33, 1)
})
it('should skip videos without cost data', () => {
const noCost: VideoWithPerformance[] = [
{ id: 10, title: 'No Cost', performance: { views: 500 } },
]
const result = calculateROI(noCost)
expect(result).toHaveLength(0)
})
it('should skip videos with zero production cost', () => {
const zeroCost: VideoWithPerformance[] = [
{
id: 10,
title: 'Zero Cost',
performance: { views: 500 },
costs: { estimatedProductionCost: 0, estimatedRevenue: 100 },
},
]
const result = calculateROI(zeroCost)
expect(result).toHaveLength(0)
})
it('should handle videos with zero views', () => {
const zeroViews: VideoWithPerformance[] = [
{
id: 10,
title: 'Zero Views',
performance: { views: 0 },
costs: { estimatedProductionCost: 100, estimatedRevenue: 0 },
},
]
const result = calculateROI(zeroViews)
expect(result).toHaveLength(1)
expect(result[0].cpv).toBe(0)
expect(result[0].revenuePerView).toBe(0)
})
it('should handle missing revenue', () => {
const noRevenue: VideoWithPerformance[] = [
{
id: 10,
title: 'No Revenue',
performance: { views: 100 },
costs: { estimatedProductionCost: 50 },
},
]
const result = calculateROI(noRevenue)
expect(result).toHaveLength(1)
expect(result[0].revenue).toBe(0)
// ROI = (0 - 50) / 50 * 100 = -100%
expect(result[0].roi).toBe(-100)
})
})
})