import { describe, test, afterEach } from "node:test"; import assert from "node:assert/strict"; import { mkdtempSync, rmSync, existsSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { getISOWeek, getCurrentISOWeek, getPostsForWeek, generateWeeklyReport, } from "../src/reports/weekly.js"; import { saveBatch, saveWeeklyReport } from "../src/utils/storage.js"; import type { PostAnalytics, AnalyticsBatch } from "../src/models/types.js"; // Helper function to create test post data function createTestPost(overrides?: Partial): PostAnalytics { return { id: overrides?.id || "test-post-1", title: overrides?.title || "Test post content", publishedDate: overrides?.publishedDate || "2026-01-15", metrics: { impressions: 1000, reactions: 50, comments: 10, shares: 5, clicks: 20, engagementRate: 8.5, ...(overrides?.metrics || {}), }, importedAt: overrides?.importedAt || "2026-01-20T10:00:00Z", exportSource: overrides?.exportSource || "test-export.csv", }; } // Helper function to create test batch data function createTestBatch(overrides?: Partial): AnalyticsBatch { const posts = overrides?.posts || [createTestPost()]; return { batchId: overrides?.batchId || "12345678-1234-1234-1234-123456789abc", importedAt: overrides?.importedAt || "2026-01-20T10:00:00Z", exportFilename: overrides?.exportFilename || "test-export.csv", dateRange: overrides?.dateRange || { from: "2026-01-15", to: "2026-01-20" }, postCount: posts.length, posts, }; } describe("weekly", () => { let tempDir: string; // Create temp directory before each test function setupTempDir(): string { return mkdtempSync(join(tmpdir(), "analytics-test-")); } // Clean up temp directory after each test afterEach(() => { if (tempDir && existsSync(tempDir)) { rmSync(tempDir, { recursive: true, force: true }); } }); describe("getISOWeek", () => { test("should return correct ISO week for 2026-01-01", () => { // 2026-01-01 is Thursday, should be week 1 of 2026 const result = getISOWeek(new Date("2026-01-01")); assert.equal(result, "2026-W01"); }); test("should return correct ISO week for 2025-12-29", () => { // 2025-12-29 is Monday, first day of ISO week 1, 2026 const result = getISOWeek(new Date("2025-12-29")); assert.equal(result, "2026-W01"); }); test("should return correct ISO week for 2025-12-28", () => { // 2025-12-28 is Sunday, last day of 2025 week 52 const result = getISOWeek(new Date("2025-12-28")); assert.equal(result, "2025-W52"); }); test("should handle year boundaries - early January", () => { // 2025-01-01 is Wednesday, should be in 2025-W01 const result = getISOWeek(new Date("2025-01-01")); assert.equal(result, "2025-W01"); }); test("should handle year boundaries - late December", () => { // 2024-12-30 is Monday, should be in 2025-W01 const result = getISOWeek(new Date("2024-12-30")); assert.equal(result, "2025-W01"); }); test("should handle mid-year dates", () => { // 2026-06-15 is Monday const result = getISOWeek(new Date("2026-06-15")); assert.equal(result, "2026-W25"); }); test("should handle leap year", () => { // 2024 is a leap year, Feb 29 should be in week 9 const result = getISOWeek(new Date("2024-02-29")); assert.equal(result, "2024-W09"); }); }); describe("getCurrentISOWeek", () => { test("should return a valid ISO week format", () => { const result = getCurrentISOWeek(); // Should match YYYY-WXX format assert.match(result, /^\d{4}-W\d{2}$/); }); test("should return a week in reasonable range", () => { const result = getCurrentISOWeek(); const year = parseInt(result.split("-")[0]); const week = parseInt(result.split("-W")[1]); // Year should be current or adjacent const currentYear = new Date().getFullYear(); assert.ok(year >= currentYear - 1 && year <= currentYear + 1); // Week should be 1-53 assert.ok(week >= 1 && week <= 53); }); }); describe("getPostsForWeek", () => { test("should filter posts to correct week", () => { const posts: PostAnalytics[] = [ createTestPost({ id: "post1", publishedDate: "2026-01-05", // 2026-W02 }), createTestPost({ id: "post2", publishedDate: "2026-01-12", // 2026-W03 }), createTestPost({ id: "post3", publishedDate: "2026-01-13", // 2026-W03 }), createTestPost({ id: "post4", publishedDate: "2026-01-19", // 2026-W04 }), ]; const week3Posts = getPostsForWeek(posts, "2026-W03"); assert.equal(week3Posts.length, 2); assert.ok(week3Posts.some(p => p.id === "post2")); assert.ok(week3Posts.some(p => p.id === "post3")); }); test("should return empty for weeks with no posts", () => { const posts: PostAnalytics[] = [ createTestPost({ id: "post1", publishedDate: "2026-01-05", // 2026-W02 }), ]; const result = getPostsForWeek(posts, "2026-W03"); assert.deepEqual(result, []); }); test("should handle empty posts array", () => { const result = getPostsForWeek([], "2026-W03"); assert.deepEqual(result, []); }); test("should handle posts across year boundary", () => { const posts: PostAnalytics[] = [ createTestPost({ id: "post1", publishedDate: "2025-12-29", // 2026-W01 (Monday) }), createTestPost({ id: "post2", publishedDate: "2026-01-01", // 2026-W01 (Thursday) }), createTestPost({ id: "post3", publishedDate: "2025-12-28", // 2025-W52 (Sunday) }), ]; const week1Posts = getPostsForWeek(posts, "2026-W01"); assert.equal(week1Posts.length, 2); assert.ok(week1Posts.some(p => p.id === "post1")); assert.ok(week1Posts.some(p => p.id === "post2")); }); }); describe("generateWeeklyReport", () => { test("should handle no posts", () => { tempDir = setupTempDir(); const report = generateWeeklyReport(tempDir, "2026-W03"); assert.equal(report.week, "2026-W03"); assert.equal(report.summary.totalPosts, 0); assert.equal(report.summary.totalImpressions, 0); assert.equal(report.summary.avgEngagementRate, 0); assert.deepEqual(report.topPerformers, []); assert.deepEqual(report.underperformers, []); }); test("should calculate correct summary metrics", () => { tempDir = setupTempDir(); const posts: PostAnalytics[] = [ createTestPost({ id: "post1", publishedDate: "2026-01-12", // 2026-W03 metrics: { impressions: 1000, reactions: 50, comments: 10, shares: 5, clicks: 20, engagementRate: 8.5, }, }), createTestPost({ id: "post2", publishedDate: "2026-01-13", // 2026-W03 metrics: { impressions: 2000, reactions: 100, comments: 20, shares: 10, clicks: 40, engagementRate: 8.5, }, }), createTestPost({ id: "post3", publishedDate: "2026-01-14", // 2026-W03 metrics: { impressions: 1500, reactions: 75, comments: 15, shares: 7, clicks: 30, engagementRate: 8.47, }, }), ]; const batch = createTestBatch({ dateRange: { from: "2026-01-12", to: "2026-01-14" }, posts, }); saveBatch(tempDir, batch); const report = generateWeeklyReport(tempDir, "2026-W03"); assert.equal(report.week, "2026-W03"); assert.equal(report.summary.totalPosts, 3); assert.equal(report.summary.totalImpressions, 4500); assert.equal(report.summary.totalReactions, 225); assert.equal(report.summary.totalComments, 45); assert.equal(report.summary.totalShares, 22); assert.equal(report.summary.totalClicks, 90); assert.equal(report.summary.avgImpressionsPerPost, 1500); // Average engagement rate: (8.5 + 8.5 + 8.47) / 3 ≈ 8.49 assert.ok(Math.abs(report.summary.avgEngagementRate - 8.49) < 0.01); }); test("should identify top performers and underperformers", () => { tempDir = setupTempDir(); const posts: PostAnalytics[] = [ createTestPost({ id: "high1", publishedDate: "2026-01-12", // 2026-W03 metrics: { impressions: 1000, reactions: 50, comments: 10, shares: 5, clicks: 20, engagementRate: 10.0, }, }), createTestPost({ id: "high2", publishedDate: "2026-01-13", // 2026-W03 metrics: { impressions: 1000, reactions: 50, comments: 10, shares: 5, clicks: 20, engagementRate: 9.0, }, }), createTestPost({ id: "medium", publishedDate: "2026-01-13", // 2026-W03 metrics: { impressions: 1000, reactions: 50, comments: 10, shares: 5, clicks: 20, engagementRate: 5.0, }, }), createTestPost({ id: "low1", publishedDate: "2026-01-14", // 2026-W03 metrics: { impressions: 1000, reactions: 50, comments: 10, shares: 5, clicks: 20, engagementRate: 3.0, }, }), createTestPost({ id: "low2", publishedDate: "2026-01-14", // 2026-W03 metrics: { impressions: 1000, reactions: 50, comments: 10, shares: 5, clicks: 20, engagementRate: 2.0, }, }), ]; const batch = createTestBatch({ dateRange: { from: "2026-01-12", to: "2026-01-14" }, posts, }); saveBatch(tempDir, batch); const report = generateWeeklyReport(tempDir, "2026-W03"); // Top 3 performers (highest engagement) assert.equal(report.topPerformers.length, 3); assert.equal(report.topPerformers[0].id, "high1"); // 10.0 assert.equal(report.topPerformers[1].id, "high2"); // 9.0 assert.equal(report.topPerformers[2].id, "medium"); // 5.0 // Bottom 3 underperformers (lowest engagement) assert.equal(report.underperformers.length, 3); assert.equal(report.underperformers[0].id, "low2"); // 2.0 assert.equal(report.underperformers[1].id, "low1"); // 3.0 assert.equal(report.underperformers[2].id, "medium"); // 5.0 }); test("should handle fewer than 3 posts", () => { tempDir = setupTempDir(); const posts: PostAnalytics[] = [ createTestPost({ id: "post1", publishedDate: "2026-01-12", // 2026-W03 metrics: { impressions: 1000, reactions: 50, comments: 10, shares: 5, clicks: 20, engagementRate: 10.0, }, }), createTestPost({ id: "post2", publishedDate: "2026-01-13", // 2026-W03 metrics: { impressions: 1000, reactions: 50, comments: 10, shares: 5, clicks: 20, engagementRate: 5.0, }, }), ]; const batch = createTestBatch({ dateRange: { from: "2026-01-12", to: "2026-01-13" }, posts, }); saveBatch(tempDir, batch); const report = generateWeeklyReport(tempDir, "2026-W03"); // Should have 2 top performers assert.equal(report.topPerformers.length, 2); // Should have 2 underperformers (same posts, reversed) assert.equal(report.underperformers.length, 2); }); test("should calculate trends when previous week data exists", () => { tempDir = setupTempDir(); // Create previous week data const prevWeekPosts: PostAnalytics[] = [ createTestPost({ id: "prev1", publishedDate: "2026-01-05", // 2026-W02 metrics: { impressions: 1000, reactions: 50, comments: 10, shares: 5, clicks: 20, engagementRate: 8.5, }, }), createTestPost({ id: "prev2", publishedDate: "2026-01-06", // 2026-W02 metrics: { impressions: 1000, reactions: 50, comments: 10, shares: 5, clicks: 20, engagementRate: 8.5, }, }), ]; const prevBatch = createTestBatch({ dateRange: { from: "2026-01-05", to: "2026-01-06" }, posts: prevWeekPosts, }); saveBatch(tempDir, prevBatch); // Generate previous week report generateWeeklyReport(tempDir, "2026-W02"); // Create current week data with higher metrics const currentWeekPosts: PostAnalytics[] = [ createTestPost({ id: "curr1", publishedDate: "2026-01-12", // 2026-W03 metrics: { impressions: 1500, reactions: 75, comments: 15, shares: 7, clicks: 30, engagementRate: 8.47, }, }), createTestPost({ id: "curr2", publishedDate: "2026-01-13", // 2026-W03 metrics: { impressions: 1500, reactions: 75, comments: 15, shares: 7, clicks: 30, engagementRate: 8.47, }, }), ]; const currentBatch = createTestBatch({ dateRange: { from: "2026-01-12", to: "2026-01-13" }, posts: currentWeekPosts, }); saveBatch(tempDir, currentBatch); const report = generateWeeklyReport(tempDir, "2026-W03"); // Current impressions: 3000, Previous: 2000 → 50% increase assert.equal(report.trends.comparedTo, "2026-W02"); assert.equal(report.trends.impressionsTrend, "up"); assert.equal(report.trends.percentChange.impressions, 50); // Engagement rate essentially the same assert.equal(report.trends.engagementTrend, "stable"); }); test("should default to stable trends when no previous week data", () => { tempDir = setupTempDir(); const posts: PostAnalytics[] = [ createTestPost({ id: "post1", publishedDate: "2026-01-12", // 2026-W03 }), ]; const batch = createTestBatch({ dateRange: { from: "2026-01-12", to: "2026-01-12" }, posts, }); saveBatch(tempDir, batch); const report = generateWeeklyReport(tempDir, "2026-W03"); assert.equal(report.trends.impressionsTrend, "stable"); assert.equal(report.trends.engagementTrend, "stable"); assert.equal(report.trends.percentChange.impressions, 0); assert.equal(report.trends.percentChange.engagement, 0); }); test("should filter posts correctly for target week", () => { tempDir = setupTempDir(); const posts: PostAnalytics[] = [ createTestPost({ id: "w02-post", publishedDate: "2026-01-05", // 2026-W02 }), createTestPost({ id: "w03-post1", publishedDate: "2026-01-12", // 2026-W03 }), createTestPost({ id: "w03-post2", publishedDate: "2026-01-13", // 2026-W03 }), createTestPost({ id: "w04-post", publishedDate: "2026-01-19", // 2026-W04 }), ]; const batch = createTestBatch({ dateRange: { from: "2026-01-05", to: "2026-01-19" }, posts, }); saveBatch(tempDir, batch); const report = generateWeeklyReport(tempDir, "2026-W03"); // Should only include W03 posts assert.equal(report.summary.totalPosts, 2); assert.equal(report.topPerformers.length, 2); assert.ok(report.topPerformers.some(p => p.id === "w03-post1")); assert.ok(report.topPerformers.some(p => p.id === "w03-post2")); }); test("should use current week if week parameter not provided", () => { tempDir = setupTempDir(); const report = generateWeeklyReport(tempDir); // Should match current ISO week format assert.match(report.week, /^\d{4}-W\d{2}$/); }); }); });