/ tests / agents / audit-report-generator.test.js
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  });