mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 18:34:13 +00:00
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>
239 lines
7.3 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|