audit-report-generator.test.js
1 /** 2 * Audit Report Generator Unit Tests 3 * Tests generateAuditReport() PDF generation with mocked score data 4 */ 5 6 import { describe, test, after } from 'node:test'; 7 import assert from 'node:assert/strict'; 8 import { existsSync, unlinkSync, mkdirSync } from 'fs'; 9 import { join } from 'path'; 10 import { tmpdir } from 'os'; 11 import sharp from 'sharp'; 12 import { generateAuditReport } from '../../src/reports/audit-report-generator.js'; 13 14 const TEST_DIR = join(tmpdir(), 'auditfix-test-reports'); 15 16 // Create test image buffers 17 const aboveFoldBuffer = await sharp({ 18 create: { width: 1440, height: 900, channels: 3, background: { r: 100, g: 150, b: 200 } }, 19 }) 20 .png() 21 .toBuffer(); 22 23 const problemCropBuffer = await sharp({ 24 create: { width: 768, height: 400, channels: 3, background: { r: 200, g: 100, b: 100 } }, 25 }) 26 .jpeg({ quality: 90 }) 27 .toBuffer(); 28 29 const sampleScoreJson = { 30 website_url: 'https://example-business.com', 31 overall_calculation: { 32 conversion_score: 58, 33 letter_grade: 'F', 34 grade_interpretation: 35 'Failing conversion potential with significant improvement opportunities.', 36 }, 37 factor_scores: { 38 headline_quality: { 39 score: 6, 40 weight: 15, 41 reasoning: 'Generic headline that does not communicate value', 42 evidence: 'Uses "Welcome to Our Website"', 43 }, 44 value_proposition: { 45 score: 7, 46 weight: 14, 47 reasoning: 'Value proposition exists but is not prominent', 48 evidence: 'Found in paragraph below fold', 49 }, 50 call_to_action: { 51 score: 4, 52 weight: 13, 53 reasoning: 'CTA is below the fold and uses generic text', 54 evidence: '"Click Here" button at 60% scroll', 55 }, 56 urgency_messaging: { 57 score: 3, 58 weight: 10, 59 reasoning: 'No urgency elements found', 60 evidence: 'No countdown, limited availability, or time-based offers', 61 }, 62 hook_engagement: { 63 score: 5, 64 weight: 9, 65 reasoning: 'Minimal engagement hooks', 66 evidence: 'No video, animation, or interactive elements', 67 }, 68 trust_signals: { 69 score: 7, 70 weight: 11, 71 reasoning: 'Some trust signals present', 72 evidence: 'Business phone number visible, basic contact page', 73 }, 74 imagery_design: { 75 score: 5, 76 weight: 8, 77 reasoning: 'Stock imagery used throughout', 78 evidence: 'Generic stock photos in hero and about sections', 79 }, 80 offer_clarity: { 81 score: 8, 82 weight: 4, 83 reasoning: 'Services are clearly listed', 84 evidence: 'Service page with pricing breakdown', 85 }, 86 unique_selling_proposition: { 87 score: 4, 88 weight: 13, 89 reasoning: 'No clear differentiator from competitors', 90 evidence: 'Generic claims without specifics', 91 }, 92 contextual_appropriateness: { 93 score: 7, 94 weight: 3, 95 reasoning: 'Content matches target audience', 96 evidence: 'Industry-appropriate language and imagery', 97 }, 98 }, 99 key_strengths: ['Clear service listing', 'Contact information visible'], 100 critical_weaknesses: ['No above-fold CTA', 'Generic headline'], 101 quick_improvement_opportunities: ['Move CTA above fold', 'Write specific headline'], 102 problem_areas: [ 103 { 104 factor: 'call_to_action', 105 description: 'Primary CTA is below the fold', 106 approximate_y_position_percent: 60, 107 severity: 'high', 108 recommendation: 'Move the primary call-to-action above the fold', 109 }, 110 ], 111 technical_assessment: { 112 ssl_enabled: true, 113 security_headers_missing: ['Content-Security-Policy', 'Permissions-Policy'], 114 mobile_responsive: true, 115 }, 116 strategic_recommendations: [ 117 { 118 priority: 1, 119 category: 'quick_win', 120 description: 'Move primary CTA above the fold', 121 expected_impact: 'high', 122 estimated_effort: 'low', 123 }, 124 { 125 priority: 2, 126 category: 'strategic', 127 description: 'Rewrite headline with specific value proposition', 128 expected_impact: 'high', 129 estimated_effort: 'medium', 130 }, 131 ], 132 }; 133 134 after(() => { 135 // Clean up test files 136 try { 137 const files = ['test-report.pdf', 'test-no-crops.pdf', 'test-minimal.pdf']; 138 for (const f of files) { 139 try { 140 unlinkSync(join(TEST_DIR, f)); 141 } catch { 142 // ignore 143 } 144 } 145 } catch { 146 // ignore 147 } 148 }); 149 150 describe('generateAuditReport', () => { 151 test('generates PDF file at specified path', async () => { 152 const outputPath = join(TEST_DIR, 'test-report.pdf'); 153 154 await generateAuditReport({ 155 domain: 'example-business.com', 156 url: 'https://example-business.com', 157 scoreJson: sampleScoreJson, 158 aboveFoldBuffer, 159 problemCrops: [ 160 { 161 factor: 'call_to_action', 162 imageBuffer: problemCropBuffer, 163 description: 'CTA is below the fold', 164 recommendation: 'Move CTA above fold', 165 severity: 'high', 166 }, 167 ], 168 outputPath, 169 }); 170 171 assert.ok(existsSync(outputPath), 'PDF file should exist'); 172 173 // PDF should be a reasonable size (at least a few KB) 174 const { statSync } = await import('fs'); 175 const stats = statSync(outputPath); 176 assert.ok(stats.size > 1000, `PDF should be >1KB, got ${stats.size}`); 177 }); 178 179 test('generates PDF without problem crops', async () => { 180 const outputPath = join(TEST_DIR, 'test-no-crops.pdf'); 181 182 await generateAuditReport({ 183 domain: 'no-crops.com', 184 url: 'https://no-crops.com', 185 scoreJson: sampleScoreJson, 186 aboveFoldBuffer, 187 problemCrops: [], 188 outputPath, 189 }); 190 191 assert.ok(existsSync(outputPath)); 192 }); 193 194 test('handles minimal scoreJson', async () => { 195 const outputPath = join(TEST_DIR, 'test-minimal.pdf'); 196 197 const minimalScore = { 198 overall_calculation: { 199 conversion_score: 45, 200 letter_grade: 'F', 201 }, 202 factor_scores: {}, 203 }; 204 205 await generateAuditReport({ 206 domain: 'minimal.com', 207 url: 'https://minimal.com', 208 scoreJson: minimalScore, 209 aboveFoldBuffer, 210 problemCrops: [], 211 outputPath, 212 }); 213 214 assert.ok(existsSync(outputPath)); 215 }); 216 });